package bpmn;

import bpmn.structure.*;
import bpmn.structure.Process;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Stack;

public class BPMNHandler extends DefaultHandler
{
	private Stack<BPMNElem> elemStack;
	private Stack<BPMNElemType> stateStack;
	private BPMNDefinition definition;
	// need to parse but
	private HashSet<String> skipSet;
	private HashSet<String> errorSet;

	BPMNDefinition getDefinition()
	{
		return definition;
	}

	@Override
	public void startDocument() throws SAXException
	{
//		System.out.println("[=== parse start ===]");
		elemStack = new Stack<>();
		stateStack = new Stack<>();
		skipSet = new HashSet<>();
		errorSet = new HashSet<>();
		definition = null;
		setInit();
	}

	@Override
	public void endDocument() throws SAXException
	{
//		System.out.println("[==== parse end ====]");
		elemStack.clear();
		elemStack = null;
		stateStack.clear();
		stateStack = null;
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
	{
		// get name
		String[] names = qName.split(":");
		String name = names[names.length - 1];

		// errors
		if (errorSet.contains(name))
			errorLog("Unexpected tag: " + name);

		// skip
		if (skipSet.contains(name))
		{
			stateStack.push(BPMNElemType.SkipTag);
			return;
		}
//		System.out.println(name);
		switch (name)
		{
			case "definitions":
			{
				BPMNDefinition def = definitionHandler(attributes);
				definition = def;
				elemStack.push(def);
				stateStack.push(BPMNElemType.Definition);
				break;
			}
			case "collaboration":
			{
				Collaboration col = collaborationHandler(attributes);
				elemStack.push(col);
				stateStack.push(BPMNElemType.Collaboration);
				break;
			}
			case "participant":
			{
				Participant p = participantHandler(attributes);
				elemStack.push(p);
				stateStack.push(BPMNElemType.Participant);
				break;
			}
			case "process":
			{
				Process p = processHandler(attributes);
				elemStack.push(p);
				stateStack.push(BPMNElemType.Process);
				break;
			}
			case "laneSet":
			{
				LaneSet ls = laneSetHandler(attributes);
				elemStack.push(ls);
				stateStack.push(BPMNElemType.LaneSet);
				break;
			}
			case "lane":
			{
				Lane l = laneHandler(attributes);
				elemStack.push(l);
				stateStack.push(BPMNElemType.Lane);
				break;
			}
			case "flowNodeRef":
			{
				stateStack.push(BPMNElemType.FlowNodeRef);
				break;
			}
			case "sendTask":
			case "receiveTask":
			case "userTask":
			case "manualTask":
			case "businessRuleTask":
			case "serviceTask":
			case "scriptTask":
			case "callActivity":
			case "task":
			{
				Task t = taskHandler(attributes);
				elemStack.push(t);
				stateStack.push(BPMNElemType.Task);
				break;
			}
			case "subProcess":
			{
				SubProcess t = subProcessHandler(attributes);
				elemStack.push(t);
				stateStack.push(BPMNElemType.SubProcess);
				break;
			}
			case "incoming":
			{
				stateStack.push(BPMNElemType.Incoming);
				break;
			}
			case "outgoing":
			{
				stateStack.push(BPMNElemType.Outgoing);
				break;
			}
			case "sequenceFlow":
			{
				SequenceFlow s = sequenceFlowHandler(attributes);
				elemStack.push(s);
				stateStack.push(BPMNElemType.SequenceFlow);
				break;
			}
			case "startEvent":
			{
				if (stateStack.peek() == BPMNElemType.Task)
					break;
				StartEvent s = startEventHandler(attributes);
				elemStack.push(s);
				stateStack.push(BPMNElemType.StartEvent);
				break;
			}
			case "endEvent":
			{
				EndEvent e = endEventHandler(attributes);
				elemStack.push(e);
				stateStack.push(BPMNElemType.EndEvent);
				break;
			}
			case "exclusiveGateway":
			{
				ExclusiveGateway eg = exclusiveGatewayHandler(attributes);
				elemStack.push(eg);
				stateStack.push(BPMNElemType.ExclusiveGateway);
				break;
			}
			case "parallelGateway":
			{
				ParallelGateway pg = parallelGatewayHandler(attributes);
				elemStack.push(pg);
				stateStack.push(BPMNElemType.ParallelGateway);
				break;
			}
			default:
				if (!names[0].equals("bpmn2") && !names[0].equals("bpmn"))
					break;
				errorLog("Unexpected value: " + name);
		}
	}

	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException
	{
		String[] names = qName.split(":");
		String name = names[names.length - 1];

		// not bpmn data tag (e.g. bpmndi)
		if (!names[0].equals("bpmn2") && !names[0].equals("bpmn"))
			return;

		// no instance
		if (name.equals("flowNodeRef") || name.equals("incoming") || name.equals("outgoing"))
		{
			stateStack.pop();
			return;
		}

		// need to skip
		if (skipSet.contains(name))
		{
			stateStack.pop();
			return;
		}

		// need to parse
		stateStack.pop();
		elemStack.pop();
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException
	{
		String s = new String(ch, start, length).trim();
		if (!s.equals(""))
		{
			BPMNElemType e = stateStack.peek();
			switch (e)
			{
				case FlowNodeRef:
				{
					flowNodeRefHandler(s);
					break;
				}
				case Incoming:
				{
					elemRefHandler(s, true);
					break;
				}
				case Outgoing:
				{
					elemRefHandler(s, false);
					break;
				}
				case SkipTag:
				{
					break;
				}
				default:
					errorLog(String.format(
							"Unexpected outside tag [%s] for characters' value [%s]",
							elemStack.peek().type, s));
			}
		}
	}

	private void setInit()
	{
		// TODO: read from config class
		String[] skips = new String[]{
				"documentation",
				"conditionExpression",
				""
		};
		String[] errors = new String[]{
				"intermediateCatchEvent",
				"intermediateThrowEvent",
				"inclusiveGateway",
				"complexGateway",
				"eventBasedGateway",
		};
		skipSet.addAll(Arrays.asList(skips));
		errorSet.addAll(Arrays.asList(errors));
	}

	private BPMNDefinition definitionHandler(Attributes attributes)
	{
		// create Definition instance
		String id = attributes.getValue("id");

		return new BPMNDefinition(id);
	}

	private Collaboration collaborationHandler(Attributes attributes)
	{
		// create Collaboration instance
		String id = attributes.getValue("id");
		Collaboration col = new Collaboration(id);

		// bind to Definition (BPMN Root)
		try
		{
			BPMNDefinition def = (BPMNDefinition) elemStack.peek();
			if (def.getCollaboration() != null)
			{
				System.out.println("Only one collaboration tag is allowed!");
				System.exit(-1);
			}
			def.setCollaboration(col);
		} catch (Exception e)
		{
			errorLog("definition", "collaboration");
		}

		return col;
	}

	private Participant participantHandler(Attributes attributes)
	{
		// create Participant instance
		String id = attributes.getValue("id");
		Participant p = new Participant(id);

		// bind to Collaboration
		try
		{
			Collaboration col = (Collaboration) elemStack.peek();
			col.addParticipant(p);
		} catch (Exception e)
		{
			errorLog("collaboration", "participant");
		}

		// set process reference
		String ref = attributes.getValue("processRef");
		p.setProcessRef(ref);

		return p;
	}

	private Process processHandler(Attributes attributes)
	{
		// create Process instance
		String id = attributes.getValue("id");
		Process p = new Process(id);

		// bind to Definition (BPMN Root)
		try
		{
			BPMNDefinition def = (BPMNDefinition) elemStack.peek();
			def.addProcess(p);
		} catch (Exception e)
		{
			errorLog("definition", "process");
		}

		// set name
		String name = attributes.getValue("name");
		p.setName(name);

		// set isExecutable
		boolean isExecutable = Boolean.parseBoolean(attributes.getValue("isExecutable"));
		p.setExecutable(isExecutable);

		return p;
	}

	private LaneSet laneSetHandler(Attributes attributes)
	{
		// create LaneSet instance
		String id = attributes.getValue("id");
		LaneSet ls = new LaneSet(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.setLaneSet(ls);
		} catch (Exception e)
		{
			errorLog("process", "laneSet");
		}

		return ls;
	}

	private Lane laneHandler(Attributes attributes)
	{
		// create Lane instance
		String id = attributes.getValue("id");
		Lane l = new Lane(id);

		// bind to LaneSet
		try
		{
			LaneSet ls = (LaneSet) elemStack.peek();
			ls.addLane(l);
		} catch (Exception e)
		{
			errorLog("laneSet", "lane");
		}

		// set Lane name
		String name = attributes.getValue("name");
		l.setName(name);

		return l;
	}

	private void flowNodeRefHandler(String ref)
	{
		// bind to Lane
		try
		{
			Lane l = (Lane) elemStack.peek();
			l.addRef(ref);
		} catch (Exception e)
		{
			errorLog("Lane", "flowNodeRef");
		}
	}

	private Task taskHandler(Attributes attributes)
	{
		// create Task instance
		String id = attributes.getValue("id");
		Task t = new Task(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(t);
		} catch (Exception e)
		{
			errorLog("process", "task");
		}

		// set Task name
		String name = attributes.getValue("name");
		t.setName(name);

		return t;
	}

	private SubProcess subProcessHandler(Attributes attributes)
	{
		// create Task instance
		String id = attributes.getValue("id");
		SubProcess t = new SubProcess(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(t);
		} catch (Exception e)
		{
			errorLog("process", "task");
		}

		// set Task name
		String name = attributes.getValue("name");
		t.setName(name);

		// set fileRef
		String ref = attributes.getValue("ref");
		t.setProcessFileRef(ref);

		return t;
	}

	private SequenceFlow sequenceFlowHandler(Attributes attributes)
	{
		// create SequenceFlow instance
		String id = attributes.getValue("id");
		SequenceFlow s = new SequenceFlow(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(s);
		} catch (Exception e)
		{
			errorLog("process", "sequenceFlow");
		}

		// set SequenceFlow name
		String name = attributes.getValue("name");
		s.setName(name);

		// set SequenceFlow sourceRef
		String sourceRef = attributes.getValue("sourceRef");
		s.addIncomingRef(sourceRef);

		// set SequenceFlow targetRef
		String targetRef = attributes.getValue("targetRef");
		s.addOutgoingRef(targetRef);

		return s;
	}

	private StartEvent startEventHandler(Attributes attributes)
	{
		// create StartEvent instance
		String id = attributes.getValue("id");
		StartEvent s = new StartEvent(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(s);
		} catch (Exception e)
		{
			errorLog("process", "startEvent");
		}

		// set StartEvent name
		String name = attributes.getValue("name");
		s.setName(name);

		return s;
	}

	private EndEvent endEventHandler(Attributes attributes)
	{
		// create EndEvent instance
		String id = attributes.getValue("id");
		EndEvent ee = new EndEvent(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(ee);
		} catch (Exception e)
		{
			errorLog("process", "endElement");
		}

		// set EndEvent name
		String name = attributes.getValue("name");
		ee.setName(name);

		return ee;
	}

	private ExclusiveGateway exclusiveGatewayHandler(Attributes attributes)
	{
		// create ExclusiveGateway instance
		String id = attributes.getValue("id");
		ExclusiveGateway eg = new ExclusiveGateway(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(eg);
		} catch (Exception e)
		{
			errorLog("process", "exclusiveGateway");
		}

		// set ExclusiveGateway name
		String name = attributes.getValue("name");
		eg.setName(name);

		return eg;
	}

	private ParallelGateway parallelGatewayHandler(Attributes attributes)
	{
		// create ExclusiveGateway instance
		String id = attributes.getValue("id");
		ParallelGateway pg = new ParallelGateway(id);

		// bind to Process
		try
		{
			Process p = (Process) elemStack.peek();
			p.addElement(pg);
		} catch (Exception e)
		{
			errorLog("process", "exclusiveGateway");
		}

		// set ExclusiveGateway name
		String name = attributes.getValue("name");
		pg.setName(name);

		return pg;
	}

	private void elemRefHandler(String ref, boolean isIncoming)
	{
		// get target type
		BPMNElemType type = elemStack.peek().type;

		// bind to Lane
		try
		{
			switch (type)
			{
				case Task:
				{
					Task t = (Task) elemStack.peek();
					t.addRef(ref, isIncoming);
					break;
				}
				case StartEvent:
				{
					StartEvent s = (StartEvent) elemStack.peek();
					s.addOutgoingRef(ref);
					break;
				}
				case EndEvent:
				{
					EndEvent s = (EndEvent) elemStack.peek();
					s.addIncomingRef(ref);
					break;
				}
				case ExclusiveGateway:
				{
					ExclusiveGateway eg = (ExclusiveGateway) elemStack.peek();
					eg.addRef(ref, isIncoming);
					break;
				}
				case ParallelGateway:
				{
					ParallelGateway pg = (ParallelGateway) elemStack.peek();
					pg.addRef(ref, isIncoming);
					break;
				}
				case SubProcess:
				{
					SubProcess sp = (SubProcess) elemStack.peek();
					sp.addRef(ref, isIncoming);
					break;
				}
				default:
					errorLog(String.format("Tag <%s> can't appear inside <%s>!",
							isIncoming ? "incoming" : "outgoing",
							type.toString().toLowerCase(Locale.ROOT)));
			}
		} catch (Exception e)
		{
			errorLog("Unexpected mismatch between BPMNElemType and BPMNElem instance!");
		}
	}

	@Deprecated
	private void incomingHandler(String ref)
	{
		// get target type
		BPMNElemType type = elemStack.peek().type;

		// bind to Lane
		try
		{
			switch (type)
			{
				case Task:
				{
					Task t = (Task) elemStack.peek();
					t.addIncomingRef(ref);
					break;
				}
				default:
					throw new IllegalStateException(String.format("Tag <incoming> can't appear inside <%s>", type
							.toString().toLowerCase(Locale.ROOT)));
			}
			Lane l = (Lane) elemStack.peek();
			l.addRef(ref);
		} catch (Exception e)
		{
			errorLog(e.getMessage());
		}
	}

	@Deprecated
	private void outgoingHandler(String ref)
	{
		// get target type
		BPMNElemType type = elemStack.peek().type;

		// bind to Lane
		try
		{
			switch (type)
			{
				case Task:
				{
					Task t = (Task) elemStack.peek();
					t.addOutgoingRef(ref);
					break;
				}
				default:
					throw new IllegalStateException(String.format("Tag <outgoing> can't appear inside <%s>", type
							.toString().toLowerCase(Locale.ROOT)));
			}
			Lane l = (Lane) elemStack.peek();
			l.addRef(ref);
		} catch (Exception e)
		{
			errorLog(e.getMessage());
		}
	}

	private void errorLog(String msg)
	{
		System.out.println(msg);
		System.exit(-1);
	}

	private void errorLog(String up, String down)
	{
		errorLog(String.format("Expected direct %s tag outsides %s!", up, down));
	}
}


